/*!
 * skrollr core
 *
 * Alexander Prinzhorn - https://github.com/Prinzhorn/skrollr
 *
 * Free to use under terms of MIT license
 */
(function(window, document, undefined) {
    'use strict';

    /*
     * Global api.
     */
    var skrollr = {
        get: function() {
            return _instance;
        },
        //Main entry point.
        init: function(options) {
            return _instance || new Skrollr(options);
        },
        VERSION: '0.6.26'
    };

    //Minify optimization.
    var hasProp = Object.prototype.hasOwnProperty;
    var Math = window.Math;
    var getStyle = window.getComputedStyle;

    //They will be filled when skrollr gets initialized.
    var documentElement;
    var body;

    var EVENT_TOUCHSTART = 'touchstart';
    var EVENT_TOUCHMOVE = 'touchmove';
    var EVENT_TOUCHCANCEL = 'touchcancel';
    var EVENT_TOUCHEND = 'touchend';

    var SKROLLABLE_CLASS = 'skrollable';
    var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + '-before';
    var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + '-between';
    var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + '-after';

    var SKROLLR_CLASS = 'skrollr';
    var NO_SKROLLR_CLASS = 'no-' + SKROLLR_CLASS;
    var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + '-desktop';
    var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + '-mobile';

    var DEFAULT_EASING = 'linear';
    var DEFAULT_DURATION = 1000;//ms
    var DEFAULT_MOBILE_DECELERATION = 0.004;//pixel/ms²

    var DEFAULT_SMOOTH_SCROLLING_DURATION = 200;//ms

    var ANCHOR_START = 'start';
    var ANCHOR_END = 'end';
    var ANCHOR_CENTER = 'center';
    var ANCHOR_BOTTOM = 'bottom';

    //The property which will be added to the DOM element to hold the ID of the skrollable.
    var SKROLLABLE_ID_DOM_PROPERTY = '___skrollable_id';

    var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i;

    var rxTrim = /^\s+|\s+$/g;

    //Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor].
    var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/;

    var rxPropValue = /\s*(@?[\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi;

    //Easing function names follow the property in square brackets.
    var rxPropEasing = /^(@?[a-z\-]+)\[(\w+)\]$/;

    var rxCamelCase = /-([a-z0-9_])/g;
    var rxCamelCaseFn = function(str, letter) {
        return letter.toUpperCase();
    };

    //Numeric values with optional sign.
    var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g;

    //Used to replace occurences of {?} with a number.
    var rxInterpolateString = /\{\?\}/g;

    //Finds rgb(a) colors, which don't use the percentage notation.
    var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g;

    //Finds all gradients.
    var rxGradient = /[a-z\-]+-gradient/g;

    //Vendor prefix. Will be set once skrollr gets initialized.
    var theCSSPrefix = '';
    var theDashedCSSPrefix = '';

    //Will be called once (when skrollr gets initialized).
    var detectCSSPrefix = function() {
        //Only relevant prefixes. May be extended.
        //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so.
        var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/;

        //Detect prefix for current browser by finding the first property using a prefix.
        if(!getStyle) {
            return;
        }

        var style = getStyle(body, null);

        for(var k in style) {
            //We check the key and if the key is a number, we check the value as well, because safari's getComputedStyle returns some weird array-like thingy.
            theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes)));

            if(theCSSPrefix) {
                break;
            }
        }

        //Did we even detect a prefix?
        if(!theCSSPrefix) {
            theCSSPrefix = theDashedCSSPrefix = '';

            return;
        }

        theCSSPrefix = theCSSPrefix[0];

        //We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff.
        if(theCSSPrefix.slice(0,1) === '-') {
            theDashedCSSPrefix = theCSSPrefix;

            //There's no logic behind these. Need a look up.
            theCSSPrefix = ({
                '-webkit-': 'webkit',
                '-moz-': 'Moz',
                '-ms-': 'ms',
                '-o-': 'O'
            })[theCSSPrefix];
        } else {
            theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-';
        }
    };

    var polyfillRAF = function() {
        var requestAnimFrame = window.requestAnimationFrame || window[theCSSPrefix.toLowerCase() + 'RequestAnimationFrame'];

        var lastTime = _now();

        if(_isMobile || !requestAnimFrame) {
            requestAnimFrame = function(callback) {
                //How long did it take to render?
                var deltaTime = _now() - lastTime;
                var delay = Math.max(0, 1000 / 60 - deltaTime);

                return window.setTimeout(function() {
                    lastTime = _now();
                    callback();
                }, delay);
            };
        }

        return requestAnimFrame;
    };

    var polyfillCAF = function() {
        var cancelAnimFrame = window.cancelAnimationFrame || window[theCSSPrefix.toLowerCase() + 'CancelAnimationFrame'];

        if(_isMobile || !cancelAnimFrame) {
            cancelAnimFrame = function(timeout) {
                return window.clearTimeout(timeout);
            };
        }

        return cancelAnimFrame;
    };

    //Built-in easing functions.
    var easings = {
        begin: function() {
            return 0;
        },
        end: function() {
            return 1;
        },
        linear: function(p) {
            return p;
        },
        quadratic: function(p) {
            return p * p;
        },
        cubic: function(p) {
            return p * p * p;
        },
        swing: function(p) {
            return (-Math.cos(p * Math.PI) / 2) + 0.5;
        },
        sqrt: function(p) {
            return Math.sqrt(p);
        },
        outCubic: function(p) {
            return (Math.pow((p - 1), 3) + 1);
        },
        //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this
        bounce: function(p) {
            var a;

            if(p <= 0.5083) {
                a = 3;
            } else if(p <= 0.8489) {
                a = 9;
            } else if(p <= 0.96208) {
                a = 27;
            } else if(p <= 0.99981) {
                a = 91;
            } else {
                return 1;
            }

            return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a);
        }
    };

    /**
     * Constructor.
     */
    function Skrollr(options) {
        documentElement = document.documentElement;
        body = document.body;

        detectCSSPrefix();

        _instance = this;

        options = options || {};

        _constants = options.constants || {};

        //We allow defining custom easings or overwrite existing.
        if(options.easing) {
            for(var e in options.easing) {
                easings[e] = options.easing[e];
            }
        }

        _edgeStrategy = options.edgeStrategy || 'set';

        _listeners = {
            //Function to be called right before rendering.
            beforerender: options.beforerender,

            //Function to be called right after finishing rendering.
            render: options.render,

            //Function to be called whenever an element with the `data-emit-events` attribute passes a keyframe.
            keyframe: options.keyframe
        };

        //forceHeight is true by default
        _forceHeight = options.forceHeight !== false;

        if(_forceHeight) {
            _scale = options.scale || 1;
        }

        _mobileDeceleration = options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION;

        _smoothScrollingEnabled = options.smoothScrolling !== false;
        _smoothScrollingDuration = options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION;

        //Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated.
        _smoothScrolling = {
            targetTop: _instance.getScrollTop()
        };

        //A custom check function may be passed.
        _isMobile = ((options.mobileCheck || function() {
            return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera);
        })());

        if(_isMobile) {
            _skrollrBody = document.getElementById('skrollr-body');

            //Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body).
            if(_skrollrBody) {
                _detect3DTransforms();
            }

            _initMobile();
            _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS], [NO_SKROLLR_CLASS]);
        } else {
            _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS], [NO_SKROLLR_CLASS]);
        }

        //Triggers parsing of elements and a first reflow.
        _instance.refresh();

        _addEvent(window, 'resize orientationchange', function() {
            var width = documentElement.clientWidth;
            var height = documentElement.clientHeight;

            //Only reflow if the size actually changed (#271).
            if(height !== _lastViewportHeight || width !== _lastViewportWidth) {
                _lastViewportHeight = height;
                _lastViewportWidth = width;

                _requestReflow = true;
            }
        });

        var requestAnimFrame = polyfillRAF();

        //Let's go.
        (function animloop(){
            _render();
            _animFrame = requestAnimFrame(animloop);
        }());

        return _instance;
    }

    /**
     * (Re)parses some or all elements.
     */
    Skrollr.prototype.refresh = function(elements) {
        var elementIndex;
        var elementsLength;
        var ignoreID = false;

        //Completely reparse anything without argument.
        if(elements === undefined) {
            //Ignore that some elements may already have a skrollable ID.
            ignoreID = true;

            _skrollables = [];
            _skrollableIdCounter = 0;

            elements = document.getElementsByTagName('*');
        } else if(elements.length === undefined) {
            //We also accept a single element as parameter.
            elements = [elements];
        }

        elementIndex = 0;
        elementsLength = elements.length;

        for(; elementIndex < elementsLength; elementIndex++) {
            var el = elements[elementIndex];
            var anchorTarget = el;
            var keyFrames = [];

            //If this particular element should be smooth scrolled.
            var smoothScrollThis = _smoothScrollingEnabled;

            //The edge strategy for this particular element.
            var edgeStrategy = _edgeStrategy;

            //If this particular element should emit keyframe events.
            var emitEvents = false;

            //If we're reseting the counter, remove any old element ids that may be hanging around.
            if(ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
                delete el[SKROLLABLE_ID_DOM_PROPERTY];
            }

            if(!el.attributes) {
                continue;
            }

            //Iterate over all attributes and search for key frame attributes.
            var attributeIndex = 0;
            var attributesLength = el.attributes.length;

            for (; attributeIndex < attributesLength; attributeIndex++) {
                var attr = el.attributes[attributeIndex];

                if(attr.name === 'data-anchor-target') {
                    anchorTarget = document.querySelector(attr.value);

                    if(anchorTarget === null) {
                        throw 'Unable to find anchor target "' + attr.value + '"';
                    }

                    continue;
                }

                //Global smooth scrolling can be overridden by the element attribute.
                if(attr.name === 'data-smooth-scrolling') {
                    smoothScrollThis = attr.value !== 'off';

                    continue;
                }

                //Global edge strategy can be overridden by the element attribute.
                if(attr.name === 'data-edge-strategy') {
                    edgeStrategy = attr.value;

                    continue;
                }

                //Is this element tagged with the `data-emit-events` attribute?
                if(attr.name === 'data-emit-events') {
                    emitEvents = true;

                    continue;
                }

                var match = attr.name.match(rxKeyframeAttribute);

                if(match === null) {
                    continue;
                }

                var kf = {
                    props: attr.value,
                    //Point back to the element as well.
                    element: el,
                    //The name of the event which this keyframe will fire, if emitEvents is
                    eventType: attr.name.replace(rxCamelCase, rxCamelCaseFn)
                };

                keyFrames.push(kf);

                var constant = match[1];

                if(constant) {
                    //Strip the underscore prefix.
                    kf.constant = constant.substr(1);
                }

                //Get the key frame offset.
                var offset = match[2];

                //Is it a percentage offset?
                if(/p$/.test(offset)) {
                    kf.isPercentage = true;
                    kf.offset = (offset.slice(0, -1) | 0) / 100;
                } else {
                    kf.offset = (offset | 0);
                }

                var anchor1 = match[3];

                //If second anchor is not set, the first will be taken for both.
                var anchor2 = match[4] || anchor1;

                //"absolute" (or "classic") mode, where numbers mean absolute scroll offset.
                if(!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) {
                    kf.mode = 'absolute';

                    //data-end needs to be calculated after all key frames are known.
                    if(anchor1 === ANCHOR_END) {
                        kf.isEnd = true;
                    } else if(!kf.isPercentage) {
                        //For data-start we can already set the key frame w/o calculations.
                        //#59: "scale" options should only affect absolute mode.
                        kf.offset = kf.offset * _scale;
                    }
                }
                //"relative" mode, where numbers are relative to anchors.
                else {
                    kf.mode = 'relative';
                    kf.anchors = [anchor1, anchor2];
                }
            }

            //Does this element have key frames?
            if(!keyFrames.length) {
                continue;
            }

            //Will hold the original style and class attributes before we controlled the element (see #80).
            var styleAttr, classAttr;

            var id;

            if(!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
                //We already have this element under control. Grab the corresponding skrollable id.
                id = el[SKROLLABLE_ID_DOM_PROPERTY];
                styleAttr = _skrollables[id].styleAttr;
                classAttr = _skrollables[id].classAttr;
            } else {
                //It's an unknown element. Asign it a new skrollable id.
                id = (el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++);
                styleAttr = el.style.cssText;
                classAttr = _getClass(el);
            }

            _skrollables[id] = {
                element: el,
                styleAttr: styleAttr,
                classAttr: classAttr,
                anchorTarget: anchorTarget,
                keyFrames: keyFrames,
                smoothScrolling: smoothScrollThis,
                edgeStrategy: edgeStrategy,
                emitEvents: emitEvents,
                lastFrameIndex: -1
            };

            _updateClass(el, [SKROLLABLE_CLASS], []);
        }

        //Reflow for the first time.
        _reflow();

        //Now that we got all key frame numbers right, actually parse the properties.
        elementIndex = 0;
        elementsLength = elements.length;

        for(; elementIndex < elementsLength; elementIndex++) {
            var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]];

            if(sk === undefined) {
                continue;
            }

            //Parse the property string to objects
            _parseProps(sk);

            //Fill key frames with missing properties from left and right
            _fillProps(sk);
        }

        return _instance;
    };

    /**
     * Transform "relative" mode to "absolute" mode.
     * That is, calculate anchor position and offset of element.
     */
    Skrollr.prototype.relativeToAbsolute = function(element, viewportAnchor, elementAnchor) {
        var viewportHeight = documentElement.clientHeight;
        var box = element.getBoundingClientRect();
        var absolute = box.top;

        //#100: IE doesn't supply "height" with getBoundingClientRect.
        var boxHeight = box.bottom - box.top;

        if(viewportAnchor === ANCHOR_BOTTOM) {
            absolute -= viewportHeight;
        } else if(viewportAnchor === ANCHOR_CENTER) {
            absolute -= viewportHeight / 2;
        }

        if(elementAnchor === ANCHOR_BOTTOM) {
            absolute += boxHeight;
        } else if(elementAnchor === ANCHOR_CENTER) {
            absolute += boxHeight / 2;
        }

        //Compensate scrolling since getBoundingClientRect is relative to viewport.
        absolute += _instance.getScrollTop();

        return (absolute + 0.5) | 0;
    };

    /**
     * Animates scroll top to new position.
     */
    Skrollr.prototype.animateTo = function(top, options) {
        options = options || {};

        var now = _now();
        var scrollTop = _instance.getScrollTop();

        //Setting this to a new value will automatically cause the current animation to stop, if any.
        _scrollAnimation = {
            startTop: scrollTop,
            topDiff: top - scrollTop,
            targetTop: top,
            duration: options.duration || DEFAULT_DURATION,
            startTime: now,
            endTime: now + (options.duration || DEFAULT_DURATION),
            easing: easings[options.easing || DEFAULT_EASING],
            done: options.done
        };

        //Don't queue the animation if there's nothing to animate.
        if(!_scrollAnimation.topDiff) {
            if(_scrollAnimation.done) {
                _scrollAnimation.done.call(_instance, false);
            }

            _scrollAnimation = undefined;
        }

        return _instance;
    };

    /**
     * Stops animateTo animation.
     */
    Skrollr.prototype.stopAnimateTo = function() {
        if(_scrollAnimation && _scrollAnimation.done) {
            _scrollAnimation.done.call(_instance, true);
        }

        _scrollAnimation = undefined;
    };

    /**
     * Returns if an animation caused by animateTo is currently running.
     */
    Skrollr.prototype.isAnimatingTo = function() {
        return !!_scrollAnimation;
    };

    Skrollr.prototype.isMobile = function() {
        return _isMobile;
    };

    Skrollr.prototype.setScrollTop = function(top, force) {
        _forceRender = (force === true);

        if(_isMobile) {
            _mobileOffset = Math.min(Math.max(top, 0), _maxKeyFrame);
        } else {
            window.scrollTo(0, top);
        }

        return _instance;
    };

    Skrollr.prototype.getScrollTop = function() {
        if(_isMobile) {
            return _mobileOffset;
        } else {
            return window.pageYOffset || documentElement.scrollTop || body.scrollTop || 0;
        }
    };

    Skrollr.prototype.getMaxScrollTop = function() {
        return _maxKeyFrame;
    };

    Skrollr.prototype.on = function(name, fn) {
        _listeners[name] = fn;

        return _instance;
    };

    Skrollr.prototype.off = function(name) {
        delete _listeners[name];

        return _instance;
    };

    Skrollr.prototype.destroy = function() {
        var cancelAnimFrame = polyfillCAF();
        cancelAnimFrame(_animFrame);
        _removeAllEvents();

        _updateClass(documentElement, [NO_SKROLLR_CLASS], [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS, SKROLLR_MOBILE_CLASS]);

        var skrollableIndex = 0;
        var skrollablesLength = _skrollables.length;

        for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
            _reset(_skrollables[skrollableIndex].element);
        }

        documentElement.style.overflow = body.style.overflow = '';
        documentElement.style.height = body.style.height = '';

        if(_skrollrBody) {
            skrollr.setStyle(_skrollrBody, 'transform', 'none');
        }

        _instance = undefined;
        _skrollrBody = undefined;
        _listeners = undefined;
        _forceHeight = undefined;
        _maxKeyFrame = 0;
        _scale = 1;
        _constants = undefined;
        _mobileDeceleration = undefined;
        _direction = 'down';
        _lastTop = -1;
        _lastViewportWidth = 0;
        _lastViewportHeight = 0;
        _requestReflow = false;
        _scrollAnimation = undefined;
        _smoothScrollingEnabled = undefined;
        _smoothScrollingDuration = undefined;
        _smoothScrolling = undefined;
        _forceRender = undefined;
        _skrollableIdCounter = 0;
        _edgeStrategy = undefined;
        _isMobile = false;
        _mobileOffset = 0;
        _translateZ = undefined;
    };

    /*
     Private methods.
     */

    var _initMobile = function() {
        var initialElement;
        var initialTouchY;
        var initialTouchX;
        var currentElement;
        var currentTouchY;
        var currentTouchX;
        var lastTouchY;
        var deltaY;

        var initialTouchTime;
        var currentTouchTime;
        var lastTouchTime;
        var deltaTime;

        _addEvent(documentElement, [EVENT_TOUCHSTART, EVENT_TOUCHMOVE, EVENT_TOUCHCANCEL, EVENT_TOUCHEND].join(' '), function(e) {
            var touch = e.changedTouches[0];

            currentElement = e.target;

            //We don't want text nodes.
            while(currentElement.nodeType === 3) {
                currentElement = currentElement.parentNode;
            }

            currentTouchY = touch.clientY;
            currentTouchX = touch.clientX;
            currentTouchTime = e.timeStamp;

            if(!rxTouchIgnoreTags.test(currentElement.tagName)) {
                e.preventDefault();
            }

            switch(e.type) {
                case EVENT_TOUCHSTART:
                    //The last element we tapped on.
                    if(initialElement) {
                        initialElement.blur();
                    }

                    _instance.stopAnimateTo();

                    initialElement = currentElement;

                    initialTouchY = lastTouchY = currentTouchY;
                    initialTouchX = currentTouchX;
                    initialTouchTime = currentTouchTime;

                    break;
                case EVENT_TOUCHMOVE:
                    //Prevent default event on touchIgnore elements in case they don't have focus yet.
                    if(rxTouchIgnoreTags.test(currentElement.tagName) && document.activeElement !== currentElement) {
                        e.preventDefault();
                    }

                    deltaY = currentTouchY - lastTouchY;
                    deltaTime = currentTouchTime - lastTouchTime;

                    _instance.setScrollTop(_mobileOffset - deltaY, true);

                    lastTouchY = currentTouchY;
                    lastTouchTime = currentTouchTime;
                    break;
                default:
                case EVENT_TOUCHCANCEL:
                case EVENT_TOUCHEND:
                    var distanceY = initialTouchY - currentTouchY;
                    var distanceX = initialTouchX - currentTouchX;
                    var distance2 = distanceX * distanceX + distanceY * distanceY;

                    //Check if it was more like a tap (moved less than 7px).
                    if(distance2 < 49) {
                        if(!rxTouchIgnoreTags.test(initialElement.tagName)) {
                            initialElement.focus();

                            //It was a tap, click the element.
                            var clickEvent = document.createEvent('MouseEvents');
                            clickEvent.initMouseEvent('click', true, true, e.view, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null);
                            initialElement.dispatchEvent(clickEvent);
                        }

                        return;
                    }

                    initialElement = undefined;

                    var speed = deltaY / deltaTime;

                    //Cap speed at 3 pixel/ms.
                    speed = Math.max(Math.min(speed, 3), -3);

                    var duration = Math.abs(speed / _mobileDeceleration);
                    var targetOffset = speed * duration + 0.5 * _mobileDeceleration * duration * duration;
                    var targetTop = _instance.getScrollTop() - targetOffset;

                    //Relative duration change for when scrolling above bounds.
                    var targetRatio = 0;

                    //Change duration proportionally when scrolling would leave bounds.
                    if(targetTop > _maxKeyFrame) {
                        targetRatio = (_maxKeyFrame - targetTop) / targetOffset;

                        targetTop = _maxKeyFrame;
                    } else if(targetTop < 0) {
                        targetRatio = -targetTop / targetOffset;

                        targetTop = 0;
                    }

                    duration = duration * (1 - targetRatio);

                    _instance.animateTo((targetTop + 0.5) | 0, {easing: 'outCubic', duration: duration});
                    break;
            }
        });

        //Just in case there has already been some native scrolling, reset it.
        window.scrollTo(0, 0);
        documentElement.style.overflow = body.style.overflow = 'hidden';
    };

    /**
     * Updates key frames which depend on others / need to be updated on resize.
     * That is "end" in "absolute" mode and all key frames in "relative" mode.
     * Also handles constants, because they may change on resize.
     */
    var _updateDependentKeyFrames = function() {
        var viewportHeight = documentElement.clientHeight;
        var processedConstants = _processConstants();
        var skrollable;
        var element;
        var anchorTarget;
        var keyFrames;
        var keyFrameIndex;
        var keyFramesLength;
        var kf;
        var skrollableIndex;
        var skrollablesLength;
        var offset;
        var constantValue;

        //First process all relative-mode elements and find the max key frame.
        skrollableIndex = 0;
        skrollablesLength = _skrollables.length;

        for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
            skrollable = _skrollables[skrollableIndex];
            element = skrollable.element;
            anchorTarget = skrollable.anchorTarget;
            keyFrames = skrollable.keyFrames;

            keyFrameIndex = 0;
            keyFramesLength = keyFrames.length;

            for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
                kf = keyFrames[keyFrameIndex];

                offset = kf.offset;
                constantValue = processedConstants[kf.constant] || 0;

                kf.frame = offset;

                if(kf.isPercentage) {
                    //Convert the offset to percentage of the viewport height.
                    offset = offset * viewportHeight;

                    //Absolute + percentage mode.
                    kf.frame = offset;
                }

                if(kf.mode === 'relative') {
                    _reset(element);

                    kf.frame = _instance.relativeToAbsolute(anchorTarget, kf.anchors[0], kf.anchors[1]) - offset;

                    _reset(element, true);
                }

                kf.frame += constantValue;

                //Only search for max key frame when forceHeight is enabled.
                if(_forceHeight) {
                    //Find the max key frame, but don't use one of the data-end ones for comparison.
                    if(!kf.isEnd && kf.frame > _maxKeyFrame) {
                        _maxKeyFrame = kf.frame;
                    }
                }
            }
        }

        //#133: The document can be larger than the maxKeyFrame we found.
        _maxKeyFrame = Math.max(_maxKeyFrame, _getDocumentHeight());

        //Now process all data-end keyframes.
        skrollableIndex = 0;
        skrollablesLength = _skrollables.length;

        for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
            skrollable = _skrollables[skrollableIndex];
            keyFrames = skrollable.keyFrames;

            keyFrameIndex = 0;
            keyFramesLength = keyFrames.length;

            for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
                kf = keyFrames[keyFrameIndex];

                constantValue = processedConstants[kf.constant] || 0;

                if(kf.isEnd) {
                    kf.frame = _maxKeyFrame - kf.offset + constantValue;
                }
            }

            skrollable.keyFrames.sort(_keyFrameComparator);
        }
    };

    /**
     * Calculates and sets the style properties for the element at the given frame.
     * @param fakeFrame The frame to render at when smooth scrolling is enabled.
     * @param actualFrame The actual frame we are at.
     */
    var _calcSteps = function(fakeFrame, actualFrame) {
        //Iterate over all skrollables.
        var skrollableIndex = 0;
        var skrollablesLength = _skrollables.length;

        for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
            var skrollable = _skrollables[skrollableIndex];
            var element = skrollable.element;
            var frame = skrollable.smoothScrolling ? fakeFrame : actualFrame;
            var frames = skrollable.keyFrames;
            var framesLength = frames.length;
            var firstFrame = frames[0];
            var lastFrame = frames[frames.length - 1];
            var beforeFirst = frame < firstFrame.frame;
            var afterLast = frame > lastFrame.frame;
            var firstOrLastFrame = beforeFirst ? firstFrame : lastFrame;
            var emitEvents = skrollable.emitEvents;
            var lastFrameIndex = skrollable.lastFrameIndex;
            var key;
            var value;

            //If we are before/after the first/last frame, set the styles according to the given edge strategy.
            if(beforeFirst || afterLast) {
                //Check if we already handled this edge case last time.
                //Note: using setScrollTop it's possible that we jumped from one edge to the other.
                if(beforeFirst && skrollable.edge === -1 || afterLast && skrollable.edge === 1) {
                    continue;
                }

                //Add the skrollr-before or -after class.
                if(beforeFirst) {
                    _updateClass(element, [SKROLLABLE_BEFORE_CLASS], [SKROLLABLE_AFTER_CLASS, SKROLLABLE_BETWEEN_CLASS]);

                    //This handles the special case where we exit the first keyframe.
                    if(emitEvents && lastFrameIndex > -1) {
                        _emitEvent(element, firstFrame.eventType, _direction);
                        skrollable.lastFrameIndex = -1;
                    }
                } else {
                    _updateClass(element, [SKROLLABLE_AFTER_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_BETWEEN_CLASS]);

                    //This handles the special case where we exit the last keyframe.
                    if(emitEvents && lastFrameIndex < framesLength) {
                        _emitEvent(element, lastFrame.eventType, _direction);
                        skrollable.lastFrameIndex = framesLength;
                    }
                }

                //Remember that we handled the edge case (before/after the first/last keyframe).
                skrollable.edge = beforeFirst ? -1 : 1;

                switch(skrollable.edgeStrategy) {
                    case 'reset':
                        _reset(element);
                        continue;
                    case 'ease':
                        //Handle this case like it would be exactly at first/last keyframe and just pass it on.
                        frame = firstOrLastFrame.frame;
                        break;
                    default:
                    case 'set':
                        var props = firstOrLastFrame.props;

                        for(key in props) {
                            if(hasProp.call(props, key)) {
                                value = _interpolateString(props[key].value);

                                //Set style or attribute.
                                if(key.indexOf('@') === 0) {
                                    element.setAttribute(key.substr(1), value);
                                } else {
                                    skrollr.setStyle(element, key, value);
                                }
                            }
                        }

                        continue;
                }
            } else {
                //Did we handle an edge last time?
                if(skrollable.edge !== 0) {
                    _updateClass(element, [SKROLLABLE_CLASS, SKROLLABLE_BETWEEN_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_AFTER_CLASS]);
                    skrollable.edge = 0;
                }
            }

            //Find out between which two key frames we are right now.
            var keyFrameIndex = 0;

            for(; keyFrameIndex < framesLength - 1; keyFrameIndex++) {
                if(frame >= frames[keyFrameIndex].frame && frame <= frames[keyFrameIndex + 1].frame) {
                    var left = frames[keyFrameIndex];
                    var right = frames[keyFrameIndex + 1];

                    for(key in left.props) {
                        if(hasProp.call(left.props, key)) {
                            var progress = (frame - left.frame) / (right.frame - left.frame);

                            //Transform the current progress using the given easing function.
                            progress = left.props[key].easing(progress);

                            //Interpolate between the two values
                            value = _calcInterpolation(left.props[key].value, right.props[key].value, progress);

                            value = _interpolateString(value);

                            //Set style or attribute.
                            if(key.indexOf('@') === 0) {
                                element.setAttribute(key.substr(1), value);
                            } else {
                                skrollr.setStyle(element, key, value);
                            }
                        }
                    }

                    //Are events enabled on this element?
                    //This code handles the usual cases of scrolling through different keyframes.
                    //The special cases of before first and after last keyframe are handled above.
                    if(emitEvents) {
                        //Did we pass a new keyframe?
                        if(lastFrameIndex !== keyFrameIndex) {
                            if(_direction === 'down') {
                                _emitEvent(element, left.eventType, _direction);
                            } else {
                                _emitEvent(element, right.eventType, _direction);
                            }

                            skrollable.lastFrameIndex = keyFrameIndex;
                        }
                    }

                    break;
                }
            }
        }
    };

    /**
     * Renders all elements.
     */
    var _render = function() {
        if(_requestReflow) {
            _requestReflow = false;
            _reflow();
        }

        //We may render something else than the actual scrollbar position.
        var renderTop = _instance.getScrollTop();

        //If there's an animation, which ends in current render call, call the callback after rendering.
        var afterAnimationCallback;
        var now = _now();
        var progress;

        //Before actually rendering handle the scroll animation, if any.
        if(_scrollAnimation) {
            //It's over
            if(now >= _scrollAnimation.endTime) {
                renderTop = _scrollAnimation.targetTop;
                afterAnimationCallback = _scrollAnimation.done;
                _scrollAnimation = undefined;
            } else {
                //Map the current progress to the new progress using given easing function.
                progress = _scrollAnimation.easing((now - _scrollAnimation.startTime) / _scrollAnimation.duration);

                renderTop = (_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0;
            }

            _instance.setScrollTop(renderTop, true);
        }
        //Smooth scrolling only if there's no animation running and if we're not forcing the rendering.
        else if(!_forceRender) {
            var smoothScrollingDiff = _smoothScrolling.targetTop - renderTop;

            //The user scrolled, start new smooth scrolling.
            if(smoothScrollingDiff) {
                _smoothScrolling = {
                    startTop: _lastTop,
                    topDiff: renderTop - _lastTop,
                    targetTop: renderTop,
                    startTime: _lastRenderCall,
                    endTime: _lastRenderCall + _smoothScrollingDuration
                };
            }

            //Interpolate the internal scroll position (not the actual scrollbar).
            if(now <= _smoothScrolling.endTime) {
                //Map the current progress to the new progress using easing function.
                progress = easings.sqrt((now - _smoothScrolling.startTime) / _smoothScrollingDuration);

                renderTop = (_smoothScrolling.startTop + progress * _smoothScrolling.topDiff) | 0;
            }
        }

        //That's were we actually "scroll" on mobile.
        if(_isMobile && _skrollrBody) {
            //Set the transform ("scroll it").
            skrollr.setStyle(_skrollrBody, 'transform', 'translate(0, ' + -(_mobileOffset) + 'px) ' + _translateZ);
        }

        //Did the scroll position even change?
        if(_forceRender || _lastTop !== renderTop) {
            //Remember in which direction are we scrolling?
            _direction = (renderTop > _lastTop) ? 'down' : (renderTop < _lastTop ? 'up' : _direction);

            _forceRender = false;

            var listenerParams = {
                curTop: renderTop,
                lastTop: _lastTop,
                maxTop: _maxKeyFrame,
                direction: _direction
            };

            //Tell the listener we are about to render.
            var continueRendering = _listeners.beforerender && _listeners.beforerender.call(_instance, listenerParams);

            //The beforerender listener function is able the cancel rendering.
            if(continueRendering !== false) {
                //Now actually interpolate all the styles.
                _calcSteps(renderTop, _instance.getScrollTop());

                //Remember when we last rendered.
                _lastTop = renderTop;

                if(_listeners.render) {
                    _listeners.render.call(_instance, listenerParams);
                }
            }

            if(afterAnimationCallback) {
                afterAnimationCallback.call(_instance, false);
            }
        }

        _lastRenderCall = now;
    };

    /**
     * Parses the properties for each key frame of the given skrollable.
     */
    var _parseProps = function(skrollable) {
        //Iterate over all key frames
        var keyFrameIndex = 0;
        var keyFramesLength = skrollable.keyFrames.length;

        for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
            var frame = skrollable.keyFrames[keyFrameIndex];
            var easing;
            var value;
            var prop;
            var props = {};

            var match;

            while((match = rxPropValue.exec(frame.props)) !== null) {
                prop = match[1];
                value = match[2];

                easing = prop.match(rxPropEasing);

                //Is there an easing specified for this prop?
                if(easing !== null) {
                    prop = easing[1];
                    easing = easing[2];
                } else {
                    easing = DEFAULT_EASING;
                }

                //Exclamation point at first position forces the value to be taken literal.
                value = value.indexOf('!') ? _parseProp(value) : [value.slice(1)];

                //Save the prop for this key frame with his value and easing function
                props[prop] = {
                    value: value,
                    easing: easings[easing]
                };
            }

            frame.props = props;
        }
    };

    /**
     * Parses a value extracting numeric values and generating a format string
     * for later interpolation of the new values in old string.
     *
     * @param val The CSS value to be parsed.
     * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7]
     * where the first element is the format string later used
     * and all following elements are the numeric value.
     */
    var _parseProp = function(val) {
        var numbers = [];

        //One special case, where floats don't work.
        //We replace all occurences of rgba colors
        //which don't use percentage notation with the percentage notation.
        rxRGBAIntegerColor.lastIndex = 0;
        val = val.replace(rxRGBAIntegerColor, function(rgba) {
            return rgba.replace(rxNumericValue, function(n) {
                return n / 255 * 100 + '%';
            });
        });

        //Handle prefixing of "gradient" values.
        //For now only the prefixed value will be set. Unprefixed isn't supported anyway.
        if(theDashedCSSPrefix) {
            rxGradient.lastIndex = 0;
            val = val.replace(rxGradient, function(s) {
                return theDashedCSSPrefix + s;
            });
        }

        //Now parse ANY number inside this string and create a format string.
        val = val.replace(rxNumericValue, function(n) {
            numbers.push(+n);
            return '{?}';
        });

        //Add the formatstring as first value.
        numbers.unshift(val);

        return numbers;
    };

    /**
     * Fills the key frames with missing left and right hand properties.
     * If key frame 1 has property X and key frame 2 is missing X,
     * but key frame 3 has X again, then we need to assign X to key frame 2 too.
     *
     * @param sk A skrollable.
     */
    var _fillProps = function(sk) {
        //Will collect the properties key frame by key frame
        var propList = {};
        var keyFrameIndex;
        var keyFramesLength;

        //Iterate over all key frames from left to right
        keyFrameIndex = 0;
        keyFramesLength = sk.keyFrames.length;

        for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
            _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
        }

        //Now do the same from right to fill the last gaps

        propList = {};

        //Iterate over all key frames from right to left
        keyFrameIndex = sk.keyFrames.length - 1;

        for(; keyFrameIndex >= 0; keyFrameIndex--) {
            _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
        }
    };

    var _fillPropForFrame = function(frame, propList) {
        var key;

        //For each key frame iterate over all right hand properties and assign them,
        //but only if the current key frame doesn't have the property by itself
        for(key in propList) {
            //The current frame misses this property, so assign it.
            if(!hasProp.call(frame.props, key)) {
                frame.props[key] = propList[key];
            }
        }

        //Iterate over all props of the current frame and collect them
        for(key in frame.props) {
            propList[key] = frame.props[key];
        }
    };

    /**
     * Calculates the new values for two given values array.
     */
    var _calcInterpolation = function(val1, val2, progress) {
        var valueIndex;
        var val1Length = val1.length;

        //They both need to have the same length
        if(val1Length !== val2.length) {
            throw 'Can\'t interpolate between "' + val1[0] + '" and "' + val2[0] + '"';
        }

        //Add the format string as first element.
        var interpolated = [val1[0]];

        valueIndex = 1;

        for(; valueIndex < val1Length; valueIndex++) {
            //That's the line where the two numbers are actually interpolated.
            interpolated[valueIndex] = val1[valueIndex] + ((val2[valueIndex] - val1[valueIndex]) * progress);
        }

        return interpolated;
    };

    /**
     * Interpolates the numeric values into the format string.
     */
    var _interpolateString = function(val) {
        var valueIndex = 1;

        rxInterpolateString.lastIndex = 0;

        return val[0].replace(rxInterpolateString, function() {
            return val[valueIndex++];
        });
    };

    /**
     * Resets the class and style attribute to what it was before skrollr manipulated the element.
     * Also remembers the values it had before reseting, in order to undo the reset.
     */
    var _reset = function(elements, undo) {
        //We accept a single element or an array of elements.
        elements = [].concat(elements);

        var skrollable;
        var element;
        var elementsIndex = 0;
        var elementsLength = elements.length;

        for(; elementsIndex < elementsLength; elementsIndex++) {
            element = elements[elementsIndex];
            skrollable = _skrollables[element[SKROLLABLE_ID_DOM_PROPERTY]];

            //Couldn't find the skrollable for this DOM element.
            if(!skrollable) {
                continue;
            }

            if(undo) {
                //Reset class and style to the "dirty" (set by skrollr) values.
                element.style.cssText = skrollable.dirtyStyleAttr;
                _updateClass(element, skrollable.dirtyClassAttr);
            } else {
                //Remember the "dirty" (set by skrollr) class and style.
                skrollable.dirtyStyleAttr = element.style.cssText;
                skrollable.dirtyClassAttr = _getClass(element);

                //Reset class and style to what it originally was.
                element.style.cssText = skrollable.styleAttr;
                _updateClass(element, skrollable.classAttr);
            }
        }
    };

    /**
     * Detects support for 3d transforms by applying it to the skrollr-body.
     */
    var _detect3DTransforms = function() {
        _translateZ = 'translateZ(0)';
        skrollr.setStyle(_skrollrBody, 'transform', _translateZ);

        var computedStyle = getStyle(_skrollrBody);
        var computedTransform = computedStyle.getPropertyValue('transform');
        var computedTransformWithPrefix = computedStyle.getPropertyValue(theDashedCSSPrefix + 'transform');
        var has3D = (computedTransform && computedTransform !== 'none') || (computedTransformWithPrefix && computedTransformWithPrefix !== 'none');

        if(!has3D) {
            _translateZ = '';
        }
    };

    /**
     * Set the CSS property on the given element. Sets prefixed properties as well.
     */
    skrollr.setStyle = function(el, prop, val) {
        var style = el.style;

        //Camel case.
        prop = prop.replace(rxCamelCase, rxCamelCaseFn).replace('-', '');

        //Make sure z-index gets a <integer>.
        //This is the only <integer> case we need to handle.
        if(prop === 'zIndex') {
            if(isNaN(val)) {
                //If it's not a number, don't touch it.
                //It could for example be "auto" (#351).
                style[prop] = val;
            } else {
                //Floor the number.
                style[prop] = '' + (val | 0);
            }
        }
        //#64: "float" can't be set across browsers. Needs to use "cssFloat" for all except IE.
        else if(prop === 'float') {
            style.styleFloat = style.cssFloat = val;
        }
        else {
            //Need try-catch for old IE.
            try {
                //Set prefixed property if there's a prefix.
                if(theCSSPrefix) {
                    style[theCSSPrefix + prop.slice(0,1).toUpperCase() + prop.slice(1)] = val;
                }

                //Set unprefixed.
                style[prop] = val;
            } catch(ignore) {}
        }
    };

    /**
     * Cross browser event handling.
     */
    var _addEvent = skrollr.addEvent = function(element, names, callback) {
        var intermediate = function(e) {
            //Normalize IE event stuff.
            e = e || window.event;

            if(!e.target) {
                e.target = e.srcElement;
            }

            if(!e.preventDefault) {
                e.preventDefault = function() {
                    e.returnValue = false;
                    e.defaultPrevented = true;
                };
            }

            return callback.call(this, e);
        };

        names = names.split(' ');

        var name;
        var nameCounter = 0;
        var namesLength = names.length;

        for(; nameCounter < namesLength; nameCounter++) {
            name = names[nameCounter];

            if(element.addEventListener) {
                element.addEventListener(name, callback, false);
            } else {
                element.attachEvent('on' + name, intermediate);
            }

            //Remember the events to be able to flush them later.
            _registeredEvents.push({
                element: element,
                name: name,
                listener: callback
            });
        }
    };

    var _removeEvent = skrollr.removeEvent = function(element, names, callback) {
        names = names.split(' ');

        var nameCounter = 0;
        var namesLength = names.length;

        for(; nameCounter < namesLength; nameCounter++) {
            if(element.removeEventListener) {
                element.removeEventListener(names[nameCounter], callback, false);
            } else {
                element.detachEvent('on' + names[nameCounter], callback);
            }
        }
    };

    var _removeAllEvents = function() {
        var eventData;
        var eventCounter = 0;
        var eventsLength = _registeredEvents.length;

        for(; eventCounter < eventsLength; eventCounter++) {
            eventData = _registeredEvents[eventCounter];

            _removeEvent(eventData.element, eventData.name, eventData.listener);
        }

        _registeredEvents = [];
    };

    var _emitEvent = function(element, name, direction) {
        if(_listeners.keyframe) {
            _listeners.keyframe.call(_instance, element, name, direction);
        }
    };

    var _reflow = function() {
        var pos = _instance.getScrollTop();

        //Will be recalculated by _updateDependentKeyFrames.
        _maxKeyFrame = 0;

        if(_forceHeight && !_isMobile) {
            //un-"force" the height to not mess with the calculations in _updateDependentKeyFrames (#216).
            body.style.height = '';
        }

        _updateDependentKeyFrames();

        if(_forceHeight && !_isMobile) {
            //"force" the height.
            body.style.height = (_maxKeyFrame + documentElement.clientHeight) + 'px';
        }

        //The scroll offset may now be larger than needed (on desktop the browser/os prevents scrolling farther than the bottom).
        if(_isMobile) {
            _instance.setScrollTop(Math.min(_instance.getScrollTop(), _maxKeyFrame));
        } else {
            //Remember and reset the scroll pos (#217).
            _instance.setScrollTop(pos, true);
        }

        _forceRender = true;
    };

    /*
     * Returns a copy of the constants object where all functions and strings have been evaluated.
     */
    var _processConstants = function() {
        var viewportHeight = documentElement.clientHeight;
        var copy = {};
        var prop;
        var value;

        for(prop in _constants) {
            value = _constants[prop];

            if(typeof value === 'function') {
                value = value.call(_instance);
            }
            //Percentage offset.
            else if((/p$/).test(value)) {
                value = (value.slice(0, -1) / 100) * viewportHeight;
            }

            copy[prop] = value;
        }

        return copy;
    };

    /*
     * Returns the height of the document.
     */
    var _getDocumentHeight = function() {
        var skrollrBodyHeight = (_skrollrBody && _skrollrBody.offsetHeight || 0);
        var bodyHeight = Math.max(skrollrBodyHeight, body.scrollHeight, body.offsetHeight, documentElement.scrollHeight, documentElement.offsetHeight, documentElement.clientHeight);

        return bodyHeight - documentElement.clientHeight;
    };

    /**
     * Returns a string of space separated classnames for the current element.
     * Works with SVG as well.
     */
    var _getClass = function(element) {
        var prop = 'className';

        //SVG support by using className.baseVal instead of just className.
        if(window.SVGElement && element instanceof window.SVGElement) {
            element = element[prop];
            prop = 'baseVal';
        }

        return element[prop];
    };

    /**
     * Adds and removes a CSS classes.
     * Works with SVG as well.
     * add and remove are arrays of strings,
     * or if remove is ommited add is a string and overwrites all classes.
     */
    var _updateClass = function(element, add, remove) {
        var prop = 'className';

        //SVG support by using className.baseVal instead of just className.
        if(window.SVGElement && element instanceof window.SVGElement) {
            element = element[prop];
            prop = 'baseVal';
        }

        //When remove is ommited, we want to overwrite/set the classes.
        if(remove === undefined) {
            element[prop] = add;
            return;
        }

        //Cache current classes. We will work on a string before passing back to DOM.
        var val = element[prop];

        //All classes to be removed.
        var classRemoveIndex = 0;
        var removeLength = remove.length;

        for(; classRemoveIndex < removeLength; classRemoveIndex++) {
            val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), ' ');
        }

        val = _trim(val);

        //All classes to be added.
        var classAddIndex = 0;
        var addLength = add.length;

        for(; classAddIndex < addLength; classAddIndex++) {
            //Only add if el not already has class.
            if(_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) {
                val += ' ' + add[classAddIndex];
            }
        }

        element[prop] = _trim(val);
    };

    var _trim = function(a) {
        return a.replace(rxTrim, '');
    };

    /**
     * Adds a space before and after the string.
     */
    var _untrim = function(a) {
        return ' ' + a + ' ';
    };

    var _now = Date.now || function() {
            return +new Date();
        };

    var _keyFrameComparator = function(a, b) {
        return a.frame - b.frame;
    };

    /*
     * Private variables.
     */

    //Singleton
    var _instance;

    /*
     A list of all elements which should be animated associated with their the metadata.
     Exmaple skrollable with two key frames animating from 100px width to 20px:

     skrollable = {
     element: <the DOM element>,
     styleAttr: <style attribute of the element before skrollr>,
     classAttr: <class attribute of the element before skrollr>,
     keyFrames: [
     {
     frame: 100,
     props: {
     width: {
     value: ['{?}px', 100],
     easing: <reference to easing function>
     }
     },
     mode: "absolute"
     },
     {
     frame: 200,
     props: {
     width: {
     value: ['{?}px', 20],
     easing: <reference to easing function>
     }
     },
     mode: "absolute"
     }
     ]
     };
     */
    var _skrollables;

    var _skrollrBody;

    var _listeners;
    var _forceHeight;
    var _maxKeyFrame = 0;

    var _scale = 1;
    var _constants;

    var _mobileDeceleration;

    //Current direction (up/down).
    var _direction = 'down';

    //The last top offset value. Needed to determine direction.
    var _lastTop = -1;

    //The last time we called the render method (doesn't mean we rendered!).
    var _lastRenderCall = _now();

    //For detecting if it actually resized (#271).
    var _lastViewportWidth = 0;
    var _lastViewportHeight = 0;

    var _requestReflow = false;

    //Will contain data about a running scrollbar animation, if any.
    var _scrollAnimation;

    var _smoothScrollingEnabled;

    var _smoothScrollingDuration;

    //Will contain settins for smooth scrolling if enabled.
    var _smoothScrolling;

    //Can be set by any operation/event to force rendering even if the scrollbar didn't move.
    var _forceRender;

    //Each skrollable gets an unique ID incremented for each skrollable.
    //The ID is the index in the _skrollables array.
    var _skrollableIdCounter = 0;

    var _edgeStrategy;


    //Mobile specific vars. Will be stripped by UglifyJS when not in use.
    var _isMobile = false;

    //The virtual scroll offset when using mobile scrolling.
    var _mobileOffset = 0;

    //If the browser supports 3d transforms, this will be filled with 'translateZ(0)' (empty string otherwise).
    var _translateZ;

    //Will contain data about registered events by skrollr.
    var _registeredEvents = [];

    //Animation frame id returned by RequestAnimationFrame (or timeout when RAF is not supported).
    var _animFrame;

    //Expose skrollr as either a global variable or a require.js module
    if(typeof define === 'function' && define.amd) {
        define('skrollr', function () {
            return skrollr;
        });
    } else if (typeof module !== 'undefined' && module.exports) {
        module.exports = skrollr;
    } else {
        window.skrollr = skrollr;
    }

}(window, document));